﻿using System;
using System.Net;
using System.Net.Sockets;
using System.Diagnostics;

namespace PacketRush.Core
{
    /// <summary>
    /// Represents the client in a server/client network.
    /// </summary>
    public class NetClient : NetBase
    {
        /// <summary>
        /// Occurs when the local client is connected.
        /// </summary>
        public event EventHandler<NetEventArgs> LocalConnected;
        /// <summary>
        /// Raises the <see cref="PacketRush.Core.NetClient.LocalConnected"/> event.
        /// </summary>
        /// <param name="netConnectionId">The id of the local connection.</param>
        protected virtual void OnLocalConnected(int netConnectionId)
        {
            if (LocalConnected != null)
            {
                LocalConnected(this, new NetEventArgs(netConnectionId));
            }
        }

        /// <summary>
        /// Occurs when a client connects to the server.
        /// </summary>
        public event EventHandler<NetEventArgs> RemoteConnected;
        /// <summary>
        /// Raises the <see cref="PacketRush.Core.NetClient.RemoteConnected"/> event.
        /// </summary>
        /// <param name="netConnectionId">The id of the connection.</param>
        protected void OnConnected(int netConnectionId)
        {
            if (RemoteConnected != null)
            {
                RemoteConnected(this, new NetEventArgs(netConnectionId));
            }
        }

        /// <summary>
        /// Occurs when a client disconnects.
        /// </summary>
        public event EventHandler<NetEventArgs> RemoteDisconnected;

        /// <summary>
        /// Raises the <see cref="PacketRush.Core.NetClient.RemoteDisconnected"/> event.
        /// </summary>
        /// <param name="netConnectionId">The id of the connection.</param>
        protected void OnDisconnected(int netConnectionId)
        {
            if (RemoteDisconnected != null)
            {
                RemoteDisconnected(this, new NetEventArgs(netConnectionId));
            }
        }

        private IClientNetworkingProvider _NetProvider;

        /// <summary>
        /// The TCP end point of server
        /// </summary>
        private EndPoint _RemoteEndPoint;

        private NetTimer _Timer = new NetTimer();


        /// <summary>
        /// Represents the connection to the server.
        /// </summary>
        public INetConnection Connection { get; private set; }

        /// <summary>
        /// Describes the connection state of this client.
        /// </summary>
        public NetClientConnectionState ConnectionState { get; set; }

        /// <summary>
        /// Creates an instance of net client.
        /// </summary>
        /// <param name="serverAddress">The address of the server to connect to.</param>
        /// <param name="serverPort">The port of the server to connect to.</param>
        public NetClient(IPAddress serverAddress, int serverPort)
            : this(new DefaultClientNetworkingProvider(), serverAddress, serverPort)
        {
        }

        /// <summary>
        /// Creates an instance of net client.
        /// </summary>
        /// <param name="networkingProvider">Networking provider for this client.</param>
        /// <param name="serverAddress">The address of the server to connect to.</param>
        /// <param name="serverPort">The port of the server to connect to.</param>
        public NetClient(IClientNetworkingProvider networkingProvider, IPAddress serverAddress, int serverPort)
        {
            _RemoteEndPoint = new IPEndPoint(serverAddress, serverPort);
            _NetProvider = networkingProvider;
            _NetProvider.ErrorAction = OnError;
            _NetProvider.ReceivePacketAction = ReceivePacket;
            _NetProvider.RunningTime = _Timer;

        }

        /// <summary>
        /// Connect to the server.
        /// </summary>
        public void Connect()
        {
            if (!_Timer.IsRunning)
            {
                _Timer.Reset();
                _Timer.Start();

                _NetProvider.Connect(_RemoteEndPoint);
                ConnectionState = NetClientConnectionState.Connecting;
            }
        }

        /// <summary>
        /// Disconnect from the server.
        /// </summary>
        public void Disconnect()
        {
            if (_Timer.IsRunning)
            {
                _NetProvider.Disconnect();
                ConnectionState = NetClientConnectionState.Disconnected;
            }
        }

        private void UpdateConnectionState()
        {
            if (Connection == null)
            {
                if (ConnectionState == NetClientConnectionState.Connecting)
                {
                    Connection = _NetProvider.AcceptConnection();
                    return;
                }
            }
            else if (Connection.Connected)
            {
                ConnectionState = Connection.Id == 0 ? NetClientConnectionState.AssigningId : NetClientConnectionState.Connected;
                return;
            }

            // TODO: What happens if Connected is false?

        }

        /// <summary>
        /// Causes the client to continue processing, this should
        /// be called in a loop as frequently as required, which would
        /// normally be as fast as possible, for instance, in a game loop
        /// </summary>
        public void Update()
        {
            //
            // Get an update of the connection state
            //
            UpdateConnectionState();

            //
            // We cannot continue if connection is null
            //
            if (Connection == null) return;

            //
            // Receives any incoming packets and hands them of to the designated services
            //
            if (ConnectionState > NetClientConnectionState.Disconnected) _NetProvider.Receive();

            //
            // Sends all queued packets
            //
            if (ConnectionState > NetClientConnectionState.Disconnected) Send();

            //
            // Allow the provider to perform any additional work
            //
            if (ConnectionState > NetClientConnectionState.Disconnected) _NetProvider.Update();
        }

        /// <summary>
        /// Send any cueued packets from all registered services.
        /// </summary>
        private void Send()
        {
            for (int serviceIndex = 0; serviceIndex < Services.Count; serviceIndex++)
            {
                INetService service = Services[serviceIndex];

                Packet packet = null;
                while ((packet = service.CollectPacket()) != null)
                {
                    packet.ServiceId = serviceIndex; // Set the ServiceId of this packet to the index of the service in the services list
                    packet.SenderId = Connection.Id; // Set the sender connection id so we know where it came from on the server

                    Send(packet);
                }
            }
        }

        private void Send(Packet packet)
        {
            if ((packet.PacketFlags & PacketFlags.Reliable) != PacketFlags.None)
            {
                packet.SequenceIndex = Connection.CreateNextPacketSequenceIndex(); // assign a sequence
                _NetProvider.SendReliable(Connection, packet);
            }
            else
            {
                _NetProvider.SendUnreliable(Connection, packet);
            }
        }

        /// <summary>
        /// Processes packets that are not handled by any service, such as
        /// connection packets.
        /// </summary>
        /// <param name="packet">The packet.</param>
        protected override void ProcessSystemPacket(Packet packet)
        {
            if (packet.ServiceId == SystemPacketTypes.ConnectionBroadcast)
                ProcessConnectionPacket(packet);
            else if (packet.ServiceId == SystemPacketTypes.DisconnectionBroadcast)
                ProcessDisconnectionPacket(packet);
            else if (packet.ServiceId == SystemPacketTypes.Ping)
                ProcessPingPacket(packet);
        }

        private void ProcessPingPacket(Packet packet)
        {
            // Send this one right back after reading out some stuff
            if (ConnectionState == NetClientConnectionState.Connected && Connection.Id > 0)
            {
                packet.SenderId = Connection.Id;
                PacketReader reader = new PacketReader((Packet)packet);
                long serverTicks = reader.ReadInt64();
                Connection.Latency = reader.ReadUInt16();

                Send(packet);
            }
        }
        private void ProcessDisconnectionPacket(Packet packet)
        {
            OnDisconnected(packet.SenderId);
        }

        private void ProcessConnectionPacket(Packet packet)
        {
            // We are the connected client, so set the connection id
            if (ConnectionState > NetClientConnectionState.Disconnected &&
                Connection.Id == 0 &&
                packet.SenderId == packet.TargetId)
            {
                // We cannot be assigned the same connection id twice!
                Debug.Assert(Connection.Id != packet.SenderId);

                Connection.Id = packet.SenderId;
                OnLocalConnected(Connection.Id);
            }
            else
            {
                OnConnected(packet.SenderId);
            }
        }

        private void ReceivePacket(byte[] rawPacket, INetConnection connection)
        {
            Packet packet = (Packet)rawPacket;
            DeliverPacketToService(packet);
        }

        /// <summary>
        /// Raises the <see cref="PacketRush.Core.NetBase.Error"/> event.
        /// </summary>
        /// <param name="args">The arguments to raise the event with.</param>
        protected override void OnError(NetErrorEventArgs args)
        {
            base.OnError(args);

            Disconnect();
        }
    }
}
